#!/usr/bin/python3

import sys, os, subprocess
import cmdtools
import logging
from argparse import ArgumentParser

VERSION="1.0"

def abspath(x):
	return os.path.abspath(os.path.normpath(os.path.expanduser(x)))

class KernelBuilderException(Exception):
	def __init__(self, code, message):
		self.message = message
		self.code = code

	def __str__(self):
		return repr(self.code) + ": " + self.message

class KernelBuilder(object):

	kern_target = {
		"x86_64" : "bzImage",
		"x86" : "bzImage"
	}

	kern_path = {
		"x86_64" : "arch/x86/boot/bzImage",
		"x86" : "arch/x86/boot/bzImage"
	}

	all_kernel_env_vars = {
		"extraversion" : "EXTRAVERSION",
		"modules_dst" : "INSTALL_MOD_PATH",
		"firmware_dst" : "INSTALL_FW_PATH",
		"kernel_cc" : "CC",
		"kernel_ld" : "LD",
		"kernel_as" : "AS"
	}

	def getKernelEnvVars(self):
		out = []
		for var, val in self.kernel_env_vars.items():
			out.append("%s=%s" % ( var, val))
		return out

	def getKernelVersion(self):
		s, o = subprocess.getstatusoutput(("cd %s; make kernelversion" % self.build_src))
		if s == 0:
			logging.info("Found kernel version %s." % o )
			return o
		else:
			raise KernelBuilderException(1, "Could not determine kernel version.")

	def generateTasks(self):
	
		tasklist = cmdtools.TaskList()

		kern_envvars = self.getKernelEnvVars()
		kern_version = None
		build_dst = self.settings['build_dst'] if self.settings['build_dst'] != None else self.build_src
		kern_target = self.kern_target[self.arch]
		kern_subpath = self.kern_path[self.arch]
		kern_abspath = os.path.join(build_dst, kern_subpath)
		build_kernel = False

		for target in self.settings['targets']:

			build_kernel = False

			logging.info("Building target: " + target)

			if target in [ 'kernel', kern_target]:
				target = kern_target
				build_kernel = True
			if target in [ kern_target, 'modules', 'scripts', 'prepare', 'all', 'vmlinux' ]:
				mo = "-j%s" % repr(self.num_cores + 1)
			else:
				mo = "-j1"
		
			all_args = [ "make" ] + kern_envvars + [ mo, target ]
			tasklist.append(cmdtools.Task(all_args, cwd=self.build_src))

			# POST-MAKE

			if build_kernel:
				if os.path.exists(kern_abspath):
					kernel_name = self.settings['kernel_name']
					if kernel_name == None:
						kernel_name = os.path.basename(kern_abspath)
					sysmap_name = "System.map"
					if self.settings['suffix']:
						kernel_name += self.settings['suffix'].replace("%k", self.kernel_version)
						sysmap_name += self.settings['suffix'].replace("%k", self.kernel_version)

					tasklist.append(cmdtools.Task(["cp", kern_abspath, self.settings['dest'] + "/" + kernel_name ]))
					tasklist.append(cmdtools.Task(["cp", os.path.join(build_dst, "System.map"), self.settings['dest'] + "/" + sysmap_name ]))
			elif target == "modules_install":
				cmdlist = [ "/sbin/depmod", "-a", "-e", "-F", os.path.join( build_dst, "System.map" ) ]
				if self.settings['modules_dst'] != '/':
					cmdlist += [ "-b", install_mod_path ]
				tasklist.append(cmdtools.Task(cmdlist, cwd=self.build_src))

		self.tasklist = tasklist

	def __init__(self, **settings):

		self.kernel_env_vars = {}
		self.tasklist = None
		self.settings = settings
		
		self.build_src = self.settings['build_src']
		self.kernel_version = self.getKernelVersion()
		s, o = subprocess.getstatusoutput("lscpu -b -e")
		self.num_cores = len(o.split('\n')) - 1

		# IDENTIFY CPU, HOST:

		s, self.arch = subprocess.getstatusoutput("uname -m")
		if self.arch not in self.kern_target:
			raise KernelBuilderException(2, "Unspported arch: %s" % self.arch)

		logging.info(os.path.basename(sys.argv[0]) + " " + VERSION +  "; Using %s CPU cores." % self.num_cores)
		logging.info("Found kernel version %s." % self.kernel_version)

		# prep kernel env vars for calling make:
		for opt, env_var in self.all_kernel_env_vars.items():
			if opt in self.settings and self.settings[opt] != None:
				self.kernel_env_vars[env_var] = self.settings[opt]

	def run(self):

		self.generateTasks()
		r = cmdtools.TaskRunner(self.tasklist)
		success = r.run()
		if not success:
			raise KernelBuilderException(3,"Build failed.")

def getCommandLine():
	
	parser = ArgumentParser(prog="xkernel")
	parser.add_argument("-e", "--extraversion", action="store", dest="extraversion", default=None, help="Set extra name to be appended to end of kernel version.")
	parser.add_argument("-v", "--verbose", action="store_true", dest="verbose", default=False, help="Enable verbose mode.")
	parser.add_argument("-p", "--pretend", action="store_true", dest="pretend", default=False, help="Enable pretend mode.")
	parser.add_argument("--kernel-cc", action="store", dest="kernel_cc", default="/usr/bin/gcc", help="Set default compiler. (default:gcc)")
	parser.add_argument("--kernel-ld", action="store", dest="kernel_ld", default="/usr/bin/ld", help="Set default linker. (default:ld)")
	parser.add_argument("--kernel-as", action="store", dest="kernel_as", default="/usr/bin/as", help="Set default assembler. (default:as)")
	parser.add_argument("--utils-cc", action="store", dest="utils_cc", default=None, help="Set C compiler for non-kernel building. (default:KERNEL_CC)")
	parser.add_argument("--build-src", action="store", dest="build_src", default="/usr/src/linux", help="Set source code location. (default:/usr/src/linux)")
	parser.add_argument("--build-dst", action="store", dest="build_dst", default=None, help="Set source code location. (default:BUILD_SRC)")
	parser.add_argument("--config", action="store", dest="config", default="/usr/src/linux/.config", help="Specify kernel config to use. (default:BUILD_SRC/.config)")
	parser.add_argument("--modules-dst", action="store", dest="modules_dst", default="/", help="Destination root filesystem for modules installation. (default:/)")
	parser.add_argument("--firmware-dst", action="store", dest="firmware_dst", default="/lib/firmware", help="Set destintation install path for firmware. (default:/lib/firmware)")
	parser.add_argument("--kernel-name", action="store", dest="kernel_name", default=None, help="Rename kernel binary to this on installation. (default: don't rename)")
	parser.add_argument("--suffix", action="store", dest="suffix", default=None, help="Add this suffix to all installed files. (default: don't add suffix)")
	parser.add_argument("--log", action="store", dest="log", default=None, help="Specify file to log compilation. (default:None)")
	parser.add_argument("--dest", action="store", dest="dest", default="/var/tmp", help="Specify destination install path. (default:/var/tmp)")
	parser.add_argument("--version", action="version", version='%(prog)s ' + VERSION)
	parser.add_argument("targets", metavar="target", nargs='+', help="Makefile targets to build.")

	return vars(parser.parse_args())

if __name__ == "__main__":
	try:
		settings = getCommandLine()
		if not settings:
			sys.exit(1)
		builder = KernelBuilder(**settings)
		if settings['pretend']:
			builder.generateTasks()
			for task in  builder.tasklist:
				print(task.cmdlist, "in", task.cwd if task.cwd else "cwd")
		else:
			builder.run()
	except KernelBuilderException as e:
		print(sys.argv[0]+": error:", e.message)
		sys.exit(e.code)

# vim: ts=4 sw=4 noet
